package com.aluka.nirvana.framework.datasource.configuration;

import com.alibaba.druid.pool.DruidDataSource;
import com.aluka.nirvana.framework.datasource.constant.NirvanaDataSource;
import com.aluka.nirvana.framework.datasource.dynamic.DynamicDataSourceRegister;
import com.aluka.nirvana.framework.datasource.model.DataSourceWrap;
import com.aluka.nirvana.framework.datasource.util.EnvironmentPaser;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.lang.NonNull;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;

import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 * 数据源配置类
 * @author gongli
 * @since 2019/12/16
 */
@Configuration
@EnableConfigurationProperties({DataSourceProperties.class, DataSourceWrap.class})
@EnableTransactionManagement
@Slf4j
public class DataSourceConfiguration implements ApplicationContextAware, BeanDefinitionRegistryPostProcessor, EnvironmentAware {

    private EncryptAware encryptAware;

    private ApplicationContext applicationContext;

    private DataSourceProperties dataSourceProperties;
    private MybatisProperties mybatisProperties;
    private JpaProperties jpaProperties;
    private Map<String , DataSourceWrap> druidDataSourceMap = Maps.newHashMap();

    private boolean jpaEnabled;
    private boolean mybatisEnabled;
    private boolean isAnnotation;
    private boolean isEncrypt;


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        try {
            encryptAware = applicationContext.getBean(EncryptAware.class);
        }catch (Exception ignore){
            // ignore
        }

    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        if(!initConfiguration()){
            log.info(">>Nirvana<< 数据源尚未初始化, 无法找到 'nirvana.database.datasource..' 相关配置!");
            return;
        }
        log.info(">>Nirvana<< 数据源初始化中...");
        log.info(">>Nirvana<< Spring Jpa 开关 [{}]", jpaEnabled);
        log.info(">>Nirvana<< Mybatis 开关 [{}]", mybatisEnabled);
        log.info(">>Nirvana<< 注解驱动 开关 [{}]", isAnnotation);
        log.info(">>Nirvana<< 加密策略 开关 [{}]", isEncrypt);
        if(isAnnotation){
            initAnnotationConfig(registry);
        }else{
            initPackageScanConfig(registry);
        }
        log.info(">>Nirvana<< 数据源初始化成功 {}", druidDataSourceMap.keySet());
    }

    /**
     * 初始化使用包扫描的的数据源配置
     * 根据配置文件中配置的Mapper文件地址以及包路径进行自动切换不同数据源
     * @param registry Bean注册类
     */
    private void initPackageScanConfig(BeanDefinitionRegistry registry) {
        druidDataSourceMap.forEach((k,v) -> {
            initDefaultDataSource(registry, k, v);
            //自定义动态初始化Mybats
            if(mybatisEnabled){
                initDefaultMybatis(registry, k, v);
            }
            //自定义动态初始化Jpa
            if(jpaEnabled){
                initDefaultJpa(registry, k, v);
            }
        });
    }

    /**
     * 初始化使用注解驱动的的数据源配置
     * 根据@TargetDataSource('数据源名称')进行数据源指定切换
     * @param registry Bean注册类
     */
    private void initAnnotationConfig(BeanDefinitionRegistry registry) {
        DynamicDataSourceRegister instance = DynamicDataSourceRegister.getInstance();
        //初始使用注解驱动的数据源配置
        instance.setDataSource(druidDataSourceMap);
        instance.registerBeanDefinitions(registry);
        druidDataSourceMap.forEach((k,v) -> {
            MybatisConfiguration mybatisConfiguration = MybatisConfiguration.getInstance(mybatisProperties);
            JpaConfiguration jpaConfiguration = JpaConfiguration.getInstance(jpaProperties);
            //自定义动态初始化Mybats
            if(mybatisEnabled && mybatisConfiguration.checkMybatisConfig(k,v)){
                mybatisConfiguration.initAnnotationMapperScan(registry,k,v,applicationContext);
            }
            //自定义动态初始化Jpa
            if(jpaEnabled && jpaConfiguration.checkJpaConfig(k,v)){
                jpaConfiguration.initAnnotationConfiguration(registry,k,v,applicationContext);
            }
        });

    }

    /**
     * 初始化默认多数据源Mybatis信息
     * @param registry Bean注册类
     * @param k 数据源名称，用于注解切换时指定具体数据源
     * @param v 数据源配置信息
     */
    private void initDefaultMybatis(BeanDefinitionRegistry registry, String k, DataSourceWrap v) {
        MybatisConfiguration mybatisConfiguration = MybatisConfiguration.getInstance(mybatisProperties);
        //检查Mybatis必须配置
        if(mybatisConfiguration.checkMybatisConfig(k,v)){
            //初始化Mybatis > SqlSessionFactory
            mybatisConfiguration.initSqlSessionFactory(registry, k, v, applicationContext);
            //初始化Mybatis > SqlSessionTemplate
            mybatisConfiguration.initSqlSessionTemplate(registry, k, applicationContext);
            //初始化Mybatis > PlatformTransactionManager
            mybatisConfiguration.initPlatformTransactionManager(registry, k, applicationContext);
            //初始化Mybats > MapperScan
            mybatisConfiguration.initMapperScan(registry, k,v, applicationContext);
        }
    }

    /**
     * 初始化默认多数据源Jpa信息
     * @param registry Bean注册类
     * @param k 数据源名称，用于注解切换时指定具体数据源
     * @param v 数据源配置信息
     */
    private void initDefaultJpa(BeanDefinitionRegistry registry, String k, DataSourceWrap v) {
        JpaConfiguration jpaConfiguration = JpaConfiguration.getInstance(jpaProperties);
        //检查Jpa必须配置
        if(jpaConfiguration.checkJpaConfig(k,v)){
            //初始化Jpa > EntityManagerFactoryBean
            jpaConfiguration.initEntityManager(registry, k, v, applicationContext,false);
            //初始化Jpa > PlatformTransactionManager
            jpaConfiguration.initPlatformTransactionManager(registry, k, v, applicationContext);
            //初始化Jpa > RepositoryScanConfig
            jpaConfiguration.initRepositoryConfig(registry, k, v, applicationContext);
        }

    }

    /**
     * 初始化默认数据源配置
     * @param registry Bean注册类
     * @param k 数据源名称，用于注解切换时指定具体数据源
     * @param v 数据源配置信息
     */
    private void initDefaultDataSource(BeanDefinitionRegistry registry, String k, DataSourceWrap v) {
        if(registry.isBeanNameInUse(k + NirvanaDataSource.DATA_SOURCE)){
            return;
        }
        GenericBeanDefinition dataSourceBean = new GenericBeanDefinition();
        dataSourceBean.setSynthetic(true);
        dataSourceBean.setInitMethodName("init");
        dataSourceBean.setDestroyMethodName("close");
        if (null != v.getIsPrimary() && v.getIsPrimary()) {
            dataSourceBean.setPrimary(true);
        }
        MutablePropertyValues mpv = dataSourceBean.getPropertyValues();
        EnvironmentPaser.propertyValuesSetting(mpv, v);
        dataSourceBean.setBeanClass(DruidDataSource.class);
        registry.registerBeanDefinition(k + NirvanaDataSource.DATA_SOURCE, dataSourceBean);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }

    public boolean initConfiguration() {
        if(dataSourceProperties != null){
            this.jpaEnabled = dataSourceProperties.getJpaEnabled();
            this.mybatisEnabled = dataSourceProperties.getMybatisEnabled();
            this.isAnnotation = dataSourceProperties.getIsAnnotation();
            this.isEncrypt = dataSourceProperties.getIsEncrypt();
            this.druidDataSourceMap = dataSourceProperties.getDatasource();
            if(this.isEncrypt){
                if(encryptAware == null){
                    log.warn("nirvana.database.isEncrypt 已配置为true，但未提供相应自定义加密策略，建议提供加密类并实现 com.aluka.nirvana.framework.datasource.configuration.EncryptAware 接口");
                    encryptAware = new EncryptAware() {
                        @Override
                        public String encode(String passwordProclaim) {
                            return Base64Utils.encodeToString(passwordProclaim.getBytes(StandardCharsets.UTF_8));
                        }

                        @Override
                        public String decode(String passwordCrypt) {
                            return new String(Base64Utils.decodeFromString(passwordCrypt));
                        }
                    };
                }
                Assert.notNull(encryptAware,"");
                druidDataSourceMap.forEach((datasourceName,datasourceWrap) -> {
                    String password = datasourceWrap.getPassword();
                    datasourceWrap.setPassword(encryptAware.decode(password));
                });
            }
        }
        return druidDataSourceMap.size() > 0;
    }

    @Override
    public void setEnvironment(@NonNull Environment environment) {
        Binder binder = Binder.get(environment);
        BindResult<DataSourceProperties> dataBaseBindResult = binder.bind("nirvana.database", DataSourceProperties.class);
        if(dataBaseBindResult.isBound()){
            this.dataSourceProperties = dataBaseBindResult.get();
        }
        BindResult<MybatisProperties> mybatisBindResult = binder.bind("mybatis", MybatisProperties.class);
        if(mybatisBindResult.isBound()){
            this.mybatisProperties = mybatisBindResult.get();
        }
        BindResult<JpaProperties> jpaBindResult = binder.bind("spring.jpa", JpaProperties.class);
        if(jpaBindResult.isBound()){
            this.jpaProperties = jpaBindResult.get();
        }
    }
}
